﻿/*
	Class: Document
	
		The Document acts is the root element of a Frontal page definition.
*/
package com.frontalcode
{
	import flash.display.*;
	import flash.events.*;
	import flash.net.*;
	import flash.utils.*;
	import flash.geom.*;
	import flash.ui.*;
	
	/**
	 * The Document class handles the <document /> tag.
	 */
	public class Document extends Container
	{
		public var stylesManager : StylesManager;
		
		/**
		 * @private
		 */
		public var fontManager : FontManager;
		
		public var externalAssetManager : ExternalAssetManager;
		public var title : String;
		public var movieParent : DisplayObjectContainer;

		/**
		 * @private
		 */
		private var rendered : Boolean = false;
		private var insertXMLCt : uint = 0;
		private var invalidateStageTaskId : uint = 0;
		private var insertedXMLSinceLastRender : Array = [ ];
		private var currentXMLList : XMLList;
		
		/*
			Variable: source
			
				This variable is part of a hack to be able to report a somewhat
				clean version of the Document's source. That is, just calling
				node.toXMLString ( ) is messy because it includes the default
				stylesheet and has all the attributes created by stylesheets
				actually applied to nodes. So until we fix that, we set the
				source to be the first XML added to the document that is not the
				default stylesheet.
		*/
		private var _source : String;
		
		/**
		 * @private
		 */
		internal var documentElementIdMap : Object = { };

		/**
		 * @private
		 */
		internal var dirtyStylesheetSelectors : Array = [ ];

		/**
		 * @private
		 */
		internal var dirtyDocElems : Array = [ ];
		
		// The following are for breaking the parse and render across multiple
		// frames.
		//
		/**
		 * @private
		 */
		internal var processDelayed : Boolean = false;
		
		/**
		 * @private
		 */
		internal var nodesProcessed : Number;
		
		private var processStartTime : Number;
		static private var MIN_NODES_TO_PROCESS : Number = 50;
	
		/**
		 * Creates a new Document instance.
		 * 
		 * <p>Options may be set through the constructor, the movie's URL or the
		 * movie's FlashVars parameter. The following options are supported:
		 * 	<table class="innertable">
		 * 		<tr><th>Option</th><th>Default</th><th>Description</th></tr>
		 * 		<tr><td>delayInitialResets</td><td>false</td><td>If true then initial resets will not be automatically dispatched.</td></tr>
		 * 		<tr><td>initiallyVisible</td><td>true</td><td>If false the document will not be automatically added to the stage.</td></tr>
		 * 		<tr><td>rendersPerFrameLimit</td><td>10</td><td>Maximum number of render iterations per frame.</td></tr>
		 * 		<tr><td>includeDefaultStylesheet</td><td>true</td><td>If false the default style sheet will not be included.</td></tr>
		 * 	</table>
		 * </p>  
		 *
		 * @param movieParent	The document this element belongs to.
		 * @param title			The element's parent in the document tree.
		 * @param options		Options affecting the behavior of this instance.
		 * 						(This object does not have built-in support for 
		 * 						any options.)
		 */
		public function Document ( movieParent : DisplayObjectContainer, title : String = "untitled", options : Object = null )
		{
			this.movieParent = movieParent;
			this.title = title;
			
			fontManager = new FontManager ( );
			stylesManager = new StylesManager ( this );
			externalAssetManager = new ExternalAssetManager ( this );
			
			super ( this, null, <document xmlns="http://frontalcode.com/" />, options );
			node.@__frontalId = id;
			
			setObjectOptionDefault ( "delayInitialResets", false );
			setObjectOptionDefault ( "initiallyVisible", true );
			setObjectOptionDefault ( "rendersPerFrameLimit", 10 );
			setObjectOptionDefault ( "includeDefaultStylesheet", true );
			setObjectOptionDefault ( "showFrontalContextMenu", true );
			setObjectOptionDefault ( "traceElems", null ); // Value, e.g.: { test1: { render: true }, test2: { render: true } }
			setObjectOptionDefault ( "traceElemInfoOnDoubleClick", false );
			
			// If true, then the XML parsing and rendering process will broken up 
			// over several frames. Any movies attached during the render phase will
			// start playing like normal and so may be out of synch with other
			// attached movies.
			//
			
			setObjectOptionDefault ( "divideParseAndRenderOverFrames", false );
			setOptionsFromLoaderParameters ( );

			if ( getObjectOption ( "traceElemInfoOnDoubleClick" ) ) 
			{
				// XXX Experimental. The double-click interaction is masking the
				// elements' normal non-debug interactions.
				//
				var debugDocument : XML = 
					<content>
						<script><![CDATA[
							document.node.addNamespace ( new Namespace ( "frontalDebug", "http://frontalcode.com/debug/" ) );
						]]></script>
						<style><![CDATA[
							*
								{
									double-click-enabled: true;
									
									@frontalDebug|onRollOver
										{
											trace ( node.name ( ), gA ( "id" ) );
										}
									@frontalDebug|onDoubleClick
										{
											trace ( node.name ( ), gA ( "id" ) );
											trace ( "marginLeft:", marginLeft, "borderLeft:", borderLeft, "paddingLeft:", paddingLeft, "width:", width, "paddingRight:", paddingRight, "borderRight:", borderRight, "marginRight:", marginRight );
											trace ( "marginTop:", marginTop, "borderTop:", borderTop, "paddingTop:", paddingTop, "height:", height, "paddingBottom:", paddingBottom, "borderBottom:", borderBottom, "marginBottom:", marginBottom );
											trace ( "matching selectors in order:" );
											for each ( var s in attributes.selectors )
											{
											   trace ( s.name );
											}
										}
								}
						]]></style>
					</content>
				insertXML ( debugDocument.copy ( ).children ( ) );
				_source = null;	// We don't want to show this in the source.
			}
			
			if ( getObjectOption ( "includeDefaultStylesheet" ) ) 
			{
				insertXML ( StylesManager.defaultStylesheet.copy ( ).children ( ) );
				_source = null;	// We don't want to show the default stylesheet in the source.
			}
			
			// Whenever our needsRender member variable is set to true, we
			// invalidate the stage. This will cause the stage to send a RENDER
			// event just before it renders. We are using this feature to
			// simulate an exitFrame event because we want to allow all of our
			// descendants to mark themselves as dirty before we actually spend
			// any time doing a render.
			//
			addListener ( movieParent.stage, Event.RENDER, renderHandler, false );
			
			setContextMenu ( );
		}
		
		/**
		 * @private
		 */
		public function setContextMenu ( ) : void
		{
			if ( getObjectOption ( "showFrontalContextMenu" ) )
			{
				var menu : ContextMenu = new ContextMenu ( );
				
				var item : ContextMenuItem = new ContextMenuItem ( "About Frontal..." );
				addListener ( item, ContextMenuEvent.MENU_ITEM_SELECT, aboutFrontalMenuItemHandler );
				menu.customItems.push ( item );
				
				item = new ContextMenuItem ( "View Frontal Source..." );
				addListener ( item, ContextMenuEvent.MENU_ITEM_SELECT, viewFrontalSourceMenuItemHandler );
				menu.customItems.push ( item );
				
				movie.contextMenu = menu;
			}
		}
		
		/**
		 * @private
		 */
		public function aboutFrontalMenuItemHandler ( event : ContextMenuEvent ) : void
		{
			navigateToURL ( new URLRequest ( "http://frontalcode.com/" ), "_blank" );			
		}
		
		/**
		 * @private
		 */
		public function viewFrontalSourceMenuItemHandler ( event : ContextMenuEvent ) : void
		{
			Debugger.viewSource ( this );
		}
		
		/**
		 * @private
		 */
		override public function destroy ( ) : void 
		{ 
			super.destroy ( );
			show ( false );
			Debugger.getInstance ( ).clearLogEntries ( this );
			StylesheetSelector.onDestroyDocument ( this );
		}
		
		/**
		 * @private
		 */
		override internal function get parentAttacher ( ) : Sprite { return movieParent as Sprite; }
		
		/*
			Function: indexDocumentElement
			
				We keep indexes based on the id and class attributes to speed up
				searches based on them.
				
				This function adds an element to the indexes.
				
			Parameters:
			
				node		- The XML node to index.
				id			- The id attribute value to use for this element.
				class		- The class attribute value to use for this element.
		*/
		/**
		 * @private
		 */
		internal var documentElementCacheByClass : Object = { };
		internal var insertedXMLElementCacheByClass : Dictionary = new Dictionary ( true );

		/**
		 * @private
		 */
		internal var documentElementCacheById : Object = { };
		internal var insertedXMLElementCacheById : Dictionary = new Dictionary ( true );
		
		/**
		 * @private
		 */
		internal function indexDocumentElement ( node : XML, idValue : String, classValue: String ) : void
		{
			if ( insertedXMLSinceLastRender.length > 0 ) var xmlSegment : XMLList = insertedXMLSinceLastRender [ insertedXMLSinceLastRender.length - 1 ];
			
			if ( idValue.length > 0 )
			{
				if ( ! documentElementCacheById.hasOwnProperty ( idValue ) ) documentElementCacheById [ idValue ] = new XMLList ( );
				if ( documentElementCacheById [ idValue ].( valueOf ( ) === node ).length ( ) == 0 ) documentElementCacheById [ idValue ] += node;
				
				// Also store it per XML snippet for the optimized style sheet
				// selector process. (These per XML snippet caches are cleared
				// after each document render.)
				//
				if ( xmlSegment != null )
				{
					if ( insertedXMLElementCacheById [ xmlSegment ] == null ) insertedXMLElementCacheById [ xmlSegment ] = { }; 
					if ( ! insertedXMLElementCacheById [ xmlSegment ].hasOwnProperty ( idValue ) ) insertedXMLElementCacheById [ xmlSegment ] [ idValue ] = new XMLList ( );
					if ( insertedXMLElementCacheById [ xmlSegment ] [ idValue ].( valueOf ( ) === node ).length ( ) == 0 ) insertedXMLElementCacheById [ xmlSegment ] [ idValue ] += node;
				}
			}
			
			if ( classValue.length > 0 )
			{
				if ( xmlSegment != null && insertedXMLElementCacheByClass [ xmlSegment ] == null ) insertedXMLElementCacheByClass [ xmlSegment ] = { };
				
				for each ( var className : String in classValue.split ( /\s+/ ) )
				{
					if ( className.length == 0 ) continue;
					if ( ! documentElementCacheByClass.hasOwnProperty ( className ) ) documentElementCacheByClass [ className ] = new XMLList ( );
					if ( documentElementCacheByClass [ className ].( valueOf ( ) === node ).length ( ) == 0 ) documentElementCacheByClass [ className ] += node;
					
					if ( xmlSegment != null )
					{
						if ( ! insertedXMLElementCacheByClass [ xmlSegment ].hasOwnProperty ( className ) ) insertedXMLElementCacheByClass [ xmlSegment ] [ className ] = new XMLList ( );
						if ( insertedXMLElementCacheByClass [ xmlSegment ] [ className ].( valueOf ( ) === node ).length ( ) == 0 ) insertedXMLElementCacheByClass [ xmlSegment ] [ className ] += node;
					}
				}
			}
		}
		
		/*
			Function: unindexDocumentElement
			
				We keep indexes based on the id and class attributes to speed up
				searches based on them.
				
				This function removes an element from the indexes.
				
			Parameters:
			
				node		- The XML node to remove.
				id			- The id attribute value to use for this element.
				class		- The class attribute value to use for this element.
		*/
		/**
		 * @private
		 */
		internal function unindexDocumentElement ( node : XML, idValue : String, classValue: String ) : void
		{
			// Note that we don't delete the nodes from the list as this will
			// also delete them from the document XML.
			//
			if ( idValue.length > 0 && documentElementCacheById.hasOwnProperty ( idValue ) ) 
			{
				var value : XMLList = new XMLList ( );
				for ( var i : uint = 0; i < documentElementCacheById [ idValue ].length ( ); i++ )
				{
					if ( documentElementCacheById [ idValue ] [ i ] !== node )
					{
						value += documentElementCacheById [ idValue ] [ i ];
					}
				}
				documentElementCacheById [ idValue ] = value;
			}
			
			if ( classValue.length > 0 )
			{
				for each ( var className : String in classValue.split ( /\s+/ ) )
				{
					if ( className.length == 0 ) continue;
					value = new XMLList ( );
					for ( i = 0; i < documentElementCacheByClass [ className ].length ( ); i++ )
					{
						if ( documentElementCacheByClass [ className ] [ i ] !== node )
						{
							value += documentElementCacheByClass [ className ] [ i ];
						}
					}
					documentElementCacheByClass [ className ] = value;
				}
			}
		}
		
		/**
		 * @private
		 */
		internal function registerDocumentElement ( element : DocumentElement ) : void { documentElementIdMap [ element.id ] = element; }
		
		/**
		 * @private
		 */
		internal function unregisterDocumentElement ( element : DocumentElement ) : void 
		{ 
			unindexDocumentElement ( element.node, element.getAttribute ( "id" ), element.getAttribute ( "class" ) );
			delete documentElementIdMap [ element.id ]; 
			
			// Dispatch the REMOVE_ELEMENT event. We don't dispatch the 
			// ADD_ELEMENT event in registerDocumentElement so that we can wait
			// until the element is actually in the DocumentElement tree. (We
			// dispatch it in the parse phase below.)
			//
			document.dispatchEvent ( new FrontalEvent ( FrontalEvent.REMOVE_ELEMENT, { docElem: element } ) );
			element.dispatchEvent ( new FrontalEvent ( FrontalEvent.REMOVE_ELEMENT ) );
		}

		public function getNodeElement ( node : * ) : DocumentElement { return documentElementIdMap [ node.@__frontalId ]; }
		
		public function open ( url : String, target : String = null ) : Document
		{
			var newDocument : Document = DocumentManager.getInstance ( ).open ( url );
			if ( target == null || Util.isEmpty.test ( target ) ) DocumentManager.getInstance ( ).removeDocument ( this );
			return newDocument;
		}
		
		public function load ( documentURL : String, parent : DocumentElement = null, insertPoint : XML = null, insertAfter : Boolean = true ) : ContainerXMLLoader
		{
			var url : URLRequest = new URLRequest ( documentURL );
			var loader : ContainerXMLLoader = new ContainerXMLLoader ( parent, insertPoint, insertAfter, url, currentXMLList );
			addListener ( loader, Event.COMPLETE, loadEventHandler );
			addListener ( loader, IOErrorEvent.IO_ERROR, loadEventHandler );
			addListener ( loader, SecurityErrorEvent.SECURITY_ERROR, loadEventHandler );
			return loader;
		}
		
		/**
		 * @private
		 */
		internal function loadEventHandler ( event : Event ) : void
		{
			var loader : ContainerXMLLoader = event.target as ContainerXMLLoader;
			removeListener ( loader, Event.COMPLETE, loadEventHandler );
			removeListener ( loader, IOErrorEvent.IO_ERROR, loadEventHandler );
			removeListener ( loader, SecurityErrorEvent.SECURITY_ERROR, loadEventHandler );
			
			if ( event.type == Event.COMPLETE )
			{
				var save : XMLList = currentXMLList;
				currentXMLList = loader.currentXMLList;
				write ( loader.data, loader.parent, loader.insertPoint, loader.insertAfter );
				currentXMLList = save;
			}
			else
			{
				Debugger.logMessage ( Debugger.ERROR, "Document", "while loading \"" + loader.request.url + "\": " + ( event as IOErrorEvent ).text );
			}
		} 
		
		public function write ( xmlString : String, parent : DocumentElement = null, insertPoint : XML = null, insertAfter : Boolean = true ) : void
		{
			setSource ( xmlString );
			
			var xmlSegment : XML;
			
			try
			{
				xmlSegment = new XML ( "<content>" + xmlString + "</content>" );
			}
			catch ( e : Error )
			{
				Debugger.logMessage ( Debugger.ERROR, "XML", e.toString ( ) );
			}
			
			if ( xmlSegment != null && xmlSegment.children ( ).length ( ) > 0 ) insertXML ( xmlSegment.children ( ), parent, insertPoint, insertAfter );
		}
		
		public function insertXML ( xmlSegment : XMLList, parent : DocumentElement = null, insertPoint : XML = null, insertAfter : Boolean = true ) : void
		{
			// We don't add this to our list of newly inserted XML if it is
			// contained by something already in the list.
			//
			if ( currentXMLList == null )
			{
				currentXMLList = xmlSegment;
			}
			else
			{
				// If insertPoint is in currentXMLList (as is the case when 
				// processing an include tag) then we want to add the xmlSegment 
				// to currentXMLList.
				//
				// XXX If this cause the list to grow to 2 nodes the it creates 
				// a new XMLList object so we may end up with extra nodes due to
				// pending includes.
				//
				if ( currentXMLList.( valueOf ( ) === insertPoint ).length ( ) > 0 ) currentXMLList += xmlSegment;
			}
			
			if ( insertedXMLSinceLastRender.indexOf ( currentXMLList ) < 0 ) insertedXMLSinceLastRender.push ( currentXMLList );
			
			if ( parent == null ) parent = this;
			
			// XXX If insertPoint is null then insertChildAfter oddly inserts 
			// the node at the front of the list. Switch this behavior.
			//
			if ( insertPoint == null ) insertAfter = ! insertAfter;
			
			if ( insertAfter )
			{
				parent.node.insertChildAfter ( insertPoint, xmlSegment );
			}
			else
			{
				parent.node.insertChildBefore ( insertPoint, xmlSegment );
			}
			
			dispatchEvent ( new FrontalEvent ( FrontalEvent.INSERT_XML ) ); 
			
			// Parse the elements and set the source of necessary.
			//
			
			var saveSource : Boolean = source == null;
			
			// XXX I think this is broken. Imagine there are two segments. The
			// first has an include and so sets processDelayed. This means the
			// second call to parseDefinition does nothing. When the parse
			// process is resumed, does the second segment get parsed? I don't
			// think so because include picks up where things left off and 
			// doesn't examine the whole document for unparsed XML. I think we
			// need to keep a queue of unparsed XML that gets handled after a
			// parse is delayed and then resumed.
			//
			// This case could be produced by writing two "include" tags to the
			// document at once.
			//
			for ( var i : uint = 0; i < xmlSegment.length ( ); i++ ) 
			{
				if ( saveSource ) setSource ( xmlSegment [ i ], true );
				parseDefinition ( parent, xmlSegment [ i ] );
			}
			
			if ( ! processDelayed ) currentXMLList = null;
		}
		
		internal function setSource ( value : *, append : Boolean = false ) : void
		{
			var text : String = value is XML ? ( value as XML ).toXMLString ( ) : value;
			if ( _source == null )
			{
				_source = text;
			}
			else if ( append )
			{
				_source += text; 
			}
		}
		
		internal function get source ( ) : String
		{
			return _source;
		}
		
		/*
			Function: releaseResets
			
				Reset all the elements still awaiting their initial reset.
		*/
		public function releaseResets ( ) : void
		{
			dispatchEvent ( new FrontalEvent ( FrontalEvent.INITIAL_RESET ) );
		}
		
		/*
			Function: parseDefinition
			
				Looks at all the nodes and creates appropriate containers.
		*/
		public function parseDefinition ( parent : DocumentElement = null, thisNode : XML = null, recursing : Boolean = false ) : void
		{
			if ( processDelayed ) return;
			
			pB ( "Document.parseDefinition" );
			
			if ( parent == null && thisNode == null )
			{
				parent = this;
				thisNode = XML ( this.node.child ( 0 ) );
			}
			
			if ( nodesProcessed >= MIN_NODES_TO_PROCESS && getObjectOption ( "divideParseAndRenderOverFrames" ) && getTimer ( ) - processStartTime > 1000 / manager.fps )
			{
				processDelayed = true;
				Scheduler.setTimeoutInFrames ( parseDefinition, 1, parent, thisNode );
				pE ( "Document.parseDefinition" );
				return;
			}
			
			var node : XML = thisNode;
			
			while ( node != null )
			{
				var nextSibling : XML = null;
				
				if ( node.parent ( ) != null && node.parent ( ).children ( ).length ( ) > node.childIndex ( ) + 1 ) nextSibling = XML ( node.parent ( ).child ( node.childIndex ( ) + 1 ) );
				
				if ( node.@__frontalFullyParsed == undefined )
				{
					var container : DocumentElement = null;
					if ( node.@__frontalId != undefined ) container = getNodeElement ( node );
					
					var attributes : Attributes;
					
					if ( container == null )
					{
						// We need to parse the node's attributes and attribute
						// style classes here solely to get the final value of
						// the "custom" attribute (if it's set at all).
						//
						
						attributes = new Attributes ( stylesManager );
						attributes.parseNode ( node );
						
						if ( node.nodeKind ( ) == "text" && parent != null && ! parent.processesOwnContent )
						{
							if ( node.parent ( ) != null )
							{
								// Insert the node into a text tag.
								//
								var textNode : XML = <text>{node}</text>;
								var index : uint = node.childIndex ( );
								node = XML ( node.parent ( ).replace ( index, textNode ).child ( index ) );
							}
							else
							{
								// This node has already been processed so skip
								// it.
								//
								node = nextSibling;
								continue;
							}
						}
						
						var classRef : Class;
						
						if ( ! Util.isEmpty.test ( attributes.getAttribute ( "custom" ) ) )
						{
							var className : String = Util.trim ( attributes.getAttribute ( "custom" ) );
							
							try
							{
								classRef = getDefinitionByName ( className ) as Class;
							}
							catch ( e : Error )
							{
								Debugger.logMessage ( Debugger.ERROR, "Document", "could not find definition for \"" + className + "\"" );
							}
						}
						else if ( node.name ( ) != null )
						{
							classRef = getClassRefByNodeName ( node.name ( ), attributes, node );
						}
						
						if ( classRef != null )
						{
							container = new classRef ( this, parent, node, null );
							parent.addChild ( container );
							dispatchEvent ( new FrontalEvent ( FrontalEvent.ADD_ELEMENT, { docElem: container } ) );
						}
						
						// We add an attribute to the node that allows us to
						// go from it to its DocumentElement via 
						// getNodeElement.
						//
						
						if ( container != null ) node.@__frontalId = container.id;
						
						nodesProcessed++;
					}
					else
					{
						attributes = container.attributes;
					}
					
					if ( node.children ( ).length ( ) > 0 && ( container == null || ! container.processesOwnContent ) )
					{
						if ( ! Util.isTrue.test ( attributes.getAttribute ( "delayParsing" ) ) || DocumentManager.getInstance ( ).getObjectOption ( "isStyleInliner" ) )
						{
							parseDefinition ( container, XML ( node.child ( 0 ) ), true );
							if ( processDelayed ) break;
							node.@__frontalFullyParsed = true;
						}
						else
						{
							// Save our children nodes for parsing later.
							//
							if ( container.unparsedChildrenNodes == null )
							{
								container.unparsedChildrenNodes = node.children ( );
								delete node.*;
							}
						}
					}
					else
					{
						node.@__frontalFullyParsed = true;
					}
					
					if ( processDelayed ) break;
				}
				
				node = nextSibling;
			}
			
			if ( ! processDelayed && ! recursing && thisNode.parent ( ) != null && thisNode.parent ( ).@__frontalFullyParsed == undefined )
			{
				// If our parent isn't fully parsed then parse it.
				//
				container = getNodeElement ( thisNode.parent ( ) );
				parseDefinition ( container.parent, container.node );
			}
			
			pE ( "Document.parseDefinition" );
		}
		
		public function getClassRefByNodeName ( nodeName : String, attributes : Attributes = null, node : XML = null ) : Class
		{
			var className : String;
			var classRef : Class;
			
			switch ( nodeName )
			{
				case "div":
					className = "com.frontalcode.Container";
					break;
				case "img":
					className = "com.frontalcode.ContainerImage";
					break;
				case "video":
					className = "com.frontalcode.ContainerVideo";
					break;
				case "text":
					className = "com.frontalcode.ContainerText";
					break;
				case "manager":
					className = "com.frontalcode.ContainerManager";
					break;
				case "transitioner":
					className = "com.frontalcode.Transitioner";
					break;
				case "style":
					className = "com.frontalcode.DocElemStyles";
					break;
				case "include":
					className = "com.frontalcode.DocElemInclude";
					break;
				case "script":
					className = "com.frontalcode.DocElemScript";
					break;
				case "template":
					className = "com.frontalcode.DocElemTemplate";
					break;
				case "form":
					className = "com.frontalcode.ContainerForm";
					break;
				case "input":
					className = "com.frontalcode.ContainerInput";
					break;
				case "alt":
					className = "com.frontalcode.DocElemAlt";
					break;
				default:
					var result : * = runInteractions ( "onGetClassRefByNodeName", { nodeName: nodeName, attributes: attributes, node: node } );
					if ( result is String )
					{
						className = result as String;
					}
					else if ( result is Class )
					{
						classRef = result as Class;
					}
					else
					{
						Debugger.logMessage ( Debugger.ERROR, "Document", "ignoring unknown tag \"" + nodeName + "\"" );
					}
					break;
			}
			
			if ( classRef == null )
			{
				try
				{
					classRef = externalAssetManager.getDefinition ( className ) as Class;
				}
				catch ( e : Error )
				{
				}
			}
			
			return classRef;
		}
				
		public function getTemplateByName ( name : String ) : XMLList { return DocElemTemplate.getTemplateByName ( this, name ); }
		
		/**
		 * @private
		 */
		override internal function get containingBlockExplicitWidth ( ) : Number 
		{ 
			return getPositiveStage ( ).width;
		}
		
		/**
		 * @private
		 */
		override internal function get containingBlockExplicitHeight ( ) : Number 
		{ 
			return getPositiveStage ( ).height;
		}
		
		/**
		 * @private
		 */
		override internal function addResizeListener ( container : Container ) : void
		{
			// assert ( container == null );
			addStageResizeListener ( );
		}
		
		override internal function stageResizeHandler ( event : Event ) : void
		{
			super.stageResizeHandler ( event );
			if ( needsRender ) render ( );
		}
		
		/*
			Function: getPositiveStage
			
				Here we return the rectangle that covers the positive x and y
				coordinate space on the stage. To do this we need to get the
				original document dimensions from the stage's LoaderInfo. In the
				first frame though that might not yet be available. If not, we
				report the positive stage to be, arbitrarily, 800x600.
		*/
		/**
		 * @private
		 */
		internal function getPositiveStage ( ) : Rectangle
		{
			var result : Rectangle = new Rectangle ( 0, 0, 800, 600 );
			
			try
			{
				var originalWidth  : Number = movieParent.root.loaderInfo.width;
				var originalHeight : Number = movieParent.root.loaderInfo.height;
	
				result = new Rectangle ( 0, 0, movieParent.stage.displayState != StageDisplayState.FULL_SCREEN ? movieParent.stage.stageWidth : movieParent.stage.fullScreenWidth, movieParent.stage.displayState != StageDisplayState.FULL_SCREEN ? movieParent.stage.stageHeight : movieParent.stage.fullScreenHeight );
				
				switch ( movieParent.stage.align )
				{
					case StageAlign.TOP:
					case StageAlign.BOTTOM:
						result.left = ( result.width - originalWidth ) / 2;
						break;
						
					case StageAlign.LEFT:
					case StageAlign.RIGHT:
						result.top = ( result.height - originalHeight ) / 2;
						break;
						
					case StageAlign.TOP_LEFT:
						break;
						
					case StageAlign.TOP_RIGHT:
						result.left = result.width - originalWidth;
						break;
						
					case StageAlign.BOTTOM_LEFT:
						result.height = originalHeight;
						break;
						
					case StageAlign.BOTTOM_RIGHT:
						result.width = originalWidth;
						result.height = originalHeight;
						break;
						
					default:
						// Centered.
						result.left = ( result.width - originalWidth ) / 2;
						result.top = ( result.height - originalHeight ) / 2;
						break;
				}
			}
			catch ( e : Error )
			{
			}
			
			return result;
		}
		
		/**
		 * @private
		 */
		override public function set needsRender ( value : Boolean ) : void
		{
			super.needsRender = value;
			if ( value )
			{
				// XXX If we have a UI component (that also listens to this 
				// event) then our listener gets one render event and then no
				// more. If we remove our previous listener and add a new one 
				// though then things work again.
				//
				removeListener ( movieParent.stage, Event.RENDER, renderHandler, false );
				addListener ( movieParent.stage, Event.RENDER, renderHandler, false );
				movieParent.stage.invalidate ( );
			
				// For whatever reason, calling invalidate on the Stage does not
				// always result in a render event. I've tried invalidating the
				// Stage every frame until we get the event and it will sometimes go
				// on forever. So we wait a frame and if we haven't seen a render
				// event then we just render.
				//
				if ( invalidateStageTaskId == 0 ) invalidateStageTaskId = Scheduler.setTimeoutInFrames ( render, 1 );
			}
		}
		
		/**
		 * @private
		 */
		internal function registerDirtyStylesheetSelector ( selector : StylesheetSelector ) : void
		{
			var index : int = dirtyStylesheetSelectors.indexOf ( selector );
			if ( index < 0 ) dirtyStylesheetSelectors.push ( selector );
		}
		
		/**
		 * @private
		 */
		internal function unregisterDirtyStylesheetSelector ( selector : StylesheetSelector ) : void
		{
			var index : int = dirtyStylesheetSelectors.indexOf ( selector );
			if ( index >= 0 ) dirtyStylesheetSelectors.splice ( index, 1 );
		}
		
		/**
		 * @private
		 */
		public function processDirtyStylesheetSelectors ( ) : void
		{
			// Match any dirty stylesheet selectors. This will let any dependent 
			// DocumentElements apply the relevant style declarations.
			//
			if ( dirtyStylesheetSelectors.length > 0 )
			{
				pB ( "Document.processDirtyStylesheetSelectors" );
				
				// First sort the selectors from general to specific. This is 
				// because for each selector we will look at what has been 
				// matched so far and if there is a more general selector that's 
				// already been matched then we'll copy its result for a 
				// starting point.
				//
				dirtyStylesheetSelectors.sort ( StylesheetSelector.compareSelectors );
				
				var matchedSelectors : Array = [ ];
				
				while ( dirtyStylesheetSelectors.length > 0 )
				{
					var selector : StylesheetSelector = dirtyStylesheetSelectors [ 0 ] as StylesheetSelector;
					selector.matchElements ( insertedXMLSinceLastRender, matchedSelectors );
					matchedSelectors.push ( selector );
				}
				
				currentXMLList = null;
				insertedXMLSinceLastRender = [ ];
				insertedXMLElementCacheByClass = new Dictionary ( true );
				insertedXMLElementCacheById = new Dictionary ( true );
				pE ( "Document.processDirtyStylesheetSelectors" );			
			}
			dirtyStylesheetSelectors = [ ];
		}
		
		/**
		 * @private
		 */
		internal function registerDirtyDocElem ( element : DocumentElement ) : void { dirtyDocElems.push ( element ); }
		
		/**
		 * @private
		 */
		public function processDirtyDocElems ( ) : void
		{
			if ( dirtyDocElems.length > 0 )
			{
				pB ( "Document.processDirtyDocElems" );
				var tmpDirtyDocElems : Array = dirtyDocElems;
				dirtyDocElems = [ ];
				for ( var i : uint = 0; i < tmpDirtyDocElems.length; i++ ) tmpDirtyDocElems [ i ].reparseAttributes ( );
				pE ( "Document.processDirtyDocElems" );			
			}
		}
		
		/**
		 * @private
		 */
		internal function renderHandler ( event : Event ) : void { render ( ); }
		
		/**
		 * @private
		 */
		override public function render ( ) : void
		{
			if ( DocumentManager.getInstance ( ).getObjectOption ( "profile" ) )
			{
				Profiler.getInstance ( ).enabled = true;
				Profiler.getInstance ( ).resetTimers ( );
			}
			
			invalidateStageTaskId = Scheduler.getInstance ( ).removeTask ( invalidateStageTaskId );
			if ( processDelayed )
			{
				needsRender = true;
				return;
			}
			
			pB ( "Document.render" );
			
			var renderCt : Number = 0;
			while ( needsRender ) 
			{	
				processDirtyStylesheetSelectors ( );
				processDirtyDocElems ( );

				// This is a degenerate version of Container.renderChildren but
				// handling the case that our parent is null and there is just
				// one Container - ourself.
				//
				initializeRender ( );
				if ( needsLayoutThisRender ) 
				{
					calculateDimensionStyles ( );
					var hasDimensions : Boolean = false;
					if ( ! isNaN ( width ) && ! isNaN ( height ) )
					{
						hasDimensions = true;
						renderChildren ( );
						layoutChildren ( true );
					}
					if ( ( isNaN ( width ) && ! ( width is String ) ) || ( isNaN ( height ) && ! ( height is String ) ) )
					{
						calculateDimensionStyles ( true );
						renderChildren ( );
						layoutChildren ( true );
						calculateDimensionStyles ( );
					}
					calculateDimensionStyles ( true );
					if ( ! hasDimensions )
					{
						renderChildren ( );
						layoutChildren ( true );
					}
					checkForResize ( );
				}
				else
				{
					renderChildren ( );
					layoutChildren ( true );
				}
				renderSelf ( );
				finalizeRender ( );
				positionChildren ( );
				positionMovie ( );
				
				if ( ! getObjectOption ( "delayInitialResets" ) ) releaseResets ( );
				
				renderCt++;
				
				if ( renderCt >= getObjectOption ( "rendersPerFrameLimit" ) )
				{
					Debugger.logMessage ( Debugger.ERROR, "Document", renderCt + " renders run in one frame. Use the document option rendersPerFrameLimit to increase the limit." );
					needsRender = false;					
					break;
				}
			}
			
			if ( ! rendered )
			{
				rendered = true;
				if ( getObjectOption ( "initiallyVisible" ) ) show ( true );
			}
			
			pE ( "Document.render" );
			
			if ( DocumentManager.getInstance ( ).getObjectOption ( "profile" ) )
			{
				Profiler.getInstance ( ).enabled = false;
				trace ( Profiler.getInstance ( ).logTimers ( ) );
			}
		}
	}
}


